Koen Deforche <koen@emweb.be> 3.1.10,2011年7月14
Wt::Dbo 是一个C++对象关系映射(ORM (Object-Relational-Mapping))库。
这个库是与 Wt 一同发布的,可用来构建数据库驱动的网页程序。 但也可完全独立使用。
这个库在数据表之上提供了一个基于类的视图,其中维护了一个由数据库对象组成的对象层次关系, 这个关系会在插入、更新和删除数据库记录时自动与数据库保持同步。C++类会被映射为数据库表,类的成员会被映射成 表中的列 ,指针及指针的集合被映射为数据 库中的关系。 被映射的类的一个对象叫做 数据库对象 (dbo) 。查询结果 可以以下形式定义:数据 库对象 、元数据或数据 元组。
我们使用了一种现代 的 C++方式 来处理映射问题。 我们不是使用XML 来描述C++类 和成员 与表和列之间 的映射规则 ,也不是使用晦涩难懂的宏, 而是使用纯 净的C++代码 来定义映射规则。
在这 个 教程中, 我们会 一路向西开发 出一个勃客示例程序 , 这个示例程序将会与随整个 库一起 发布的那个 勃客示例程序类似。
提示 |
Wt 的 examples/feature/dbo/ 目录中已经包含了此教程 中所用示例 的完整代码 ,可直接编译后运行。 |
我们从简单的例子开始,使用 Wt::Dbo 来将单个的类 User 映射到 user 这个数据库表。
警告 |
在这个教程以及示例中, 我们会 为 Wt::Dbo 命名空间 起个别名 dbo 。而在我们的解说当中,会直接引用那个完整命名空间 中的类型 和方法。 |
在编译以下示例的时候,妳需要链接至 wtdbo 和 wtdbosqlite3 库。
映射单个类(tutorial1.C)
#include <Wt/Dbo/Dbo>
#include <string>
namespace dbo = Wt :: Dbo ;
class User {
public :
enum Role {
Visitor = 0 ,
Admin = 1 ,
Alien = 42
} ;
std :: string name ;
std :: string password ;
Role role ;
int karma ;
template < class Action >
void persist ( Action & a )
{
dbo :: field ( a , name , "name" );
dbo :: field ( a , password , "password" );
dbo :: field ( a , role , "role" );
dbo :: field ( a , karma , "karma" );
}
} ;
这个示例演示了,如何为一个C++类定义持久 化支持。 这里定义了一个模板成员函数 persist() , 它就是这个类的持久化定义。对于 这个类中的每个成员, 都会调用 Wt::Dbo::field() 来将它映射为数据 库 表 中的一个列。
妳可能已经注意 到了, 这个库已经支持标准 的C++ 类型 ( 在 Wt::Dbo::sql_value_traits< T > 文档中可找到一份完整的 受 支持类型列表 ) 了,例如 int 、 std::string 和 enum 类型。如果 想为其它类型提供支持的话,只需要特化 Wt::Dbo::sql_value_traits< T > 就可以了。另外 , 它还支持Wt 内置的一些类型 ,例如WDate 、 WDateTime 、 WTime 和 WString,只需要包含<Wt/Dbo/WtSqlTraits>。
这个库定义了一系列的动作(actions) ,它们 会在类的 persist() 方法中依次 针对 类的所有成员应用 到一个数据库对象上 。 这些动作对象会读取 、更新、或插入数据 库对象 , 会创建数据 库模式, 会传递事务结果 。
注意 |
简单起见,我们 在这个示例中使用了公有成员。实际 上妳完全可以 将类的状态封装成私有成员 ,并且提供访问函数。 妳甚至还可以根据访问函数 来定义持久 化函数 ,以便 在读取和写入过程中使用不同的动作对象。 |
既然我们已经 为 User 类定义好了映射规则, 那我们就可以开始干活 了: 搞出一个数据库会话, (必要的时候)创建数据库模式, 再向数据库里添加一个用户。
让我们 将代码过一遍。
(tutorial1.C继续)
void run ()
{
/*
* 创建 一个会话, 一般情况下 ,只需在程序启动 时 做一次 。
*/
dbo :: backend :: Sqlite3 sqlite3 ( "blog.db" );
dbo :: Session session ;
session . setConnection ( sqlite3 );
...
Session 对象是一个长久存在的对象,提供 了对我们的数据库对象进行访问的功能。 一般情况下, 在由同一个用户引发的整个程序会话时间里,妳只需要创建并使用 同一个Session 对象。 Wt::Dbo 中的所有类都不是线程安全的(连接 池是例外 ), 而数据库会话对象是不 会在 程序会话 之间共享的。
我们并不是因为偷懒而不提供线程安全性的。 它是符合数据库 的事务完整性承诺的: 妳不希望妳在某个会话中 还没提交的变更直接 就出现在另一个会话中了(提交 读( Read-Committed )事务隔离级别 )。当然 ,未来可能 有必要实现一个 写时复制(copy-on-write)策略, 这样就可以 在会话之间共享 大庹的数据库对象了。
要向会话提供一个连接对象, 它使用这个连接对象来与数据库通信。会话只在事务 的持续过程中 才会真正使用连接对象 ,因此 , 不需要给它提供一个独立的连接。 如果 妳 想 要实现多个并行会话 的话,那么使用连接 池会有更好的效果 ,同时,会话对象也可以使用 连接 池 的引用来初始化。
Wt::Dbo使用一个抽象层来访问数据库 ,目前支持 Postgres 和 Sqlite3 两种后端 。
(tutorial1.C继续)
...
session . mapClass < User >( "user" );
/*
* 尝试创建模式(如果已经 有了,则会失败 ) 。
*/
session . createTables ();
...
然后, 我们使用 mapClass() 将每个数据 库类注册到会话中, 以表明要将数据 表映射到哪个类上。
显然 ,在开发过程中,以及在初始部署的过程中, 让 Wt::Dbo 来创建或删除数据库模式是狠方便的。
这将会生成以下结构化查询语言(SQL)语句:
begin transaction
create table "user" (
"id" integer primary key autoincrement ,
"version" integer not null ,
"name" text not null ,
"password" text not null ,
"role" integer not null ,
"karma" integer not null
)
commit transaction
妳看到了吧, 除了 那4个映射 到C++字段的列之外,默认情况 下, Wt::Dbo 还添加了另两个列: id 和 version 。id是一个代理(surrogate)主键, 而version被用作一个基于版本的乐观(optimistic)锁。自从Wt 3.1.4 以来,在Wt::Dbo 中, 妳可以禁用version 字段 ,并且 可使用任何类型 的自然 主键,而不是使用代理 主键,参考 对映射进行自定义 。
好了,我们可以向数据库中添加一个用户了。所有 的数据库操作都在一个事务中发生。
(tutorial1.C继续)
...
/*
* 每一个小单元的工作都是在一个事务之内发生的。
*/
dbo :: Transaction transaction ( session );
User * user = new User ();
user -> name = "Joe" ;
user -> password = "Secret" ;
user -> role = User :: Visitor ;
user -> karma = 13 ;
dbo :: ptr<User> userPtr = session . add ( user );
transaction . commit ();
}
调用Session::add() 就会向数据库中添加一个对象。 这个调用会返回一个 ptr< Dbo > ,它指向一个类型为 Dbo 的数据库对象。 这是一个共享指针, 它同时会跟踪 被引用的对象的持久化状态。 在每个会话当中,单个数据 库对象最多只会被载入一次:会话 会跟踪那些已载入的数据库对象 ,并且每当 有数据 库 查询命中 了已有的数据库对象时直接返回该对象。 当某个数据 库对象的最后一个指针离开 其作用域时 , 该数据库对象的 临时 (位于内存 中 )副本 也会被删除(除非 它已被修改了, 在那种情况下, 会在将变更提交到数据库之后才删除副本 )。
会话还会跟踪那些 已被修改因而需要刷新(使用结构 化查询语言语句 )到数据库中去的对象 。刷新 会在以下情况下自动发生:提交事务 ;或者 ,需要维护临时副本 与数据库内容之间 的一致性之时(例如,进行 一个查询之前 )。
这将生成以下结构化查询语言语句:
begin transaction
insert into "user" ( "version" , "name" , "password" , "role" , "karma" ) values (?, ?, ?, ?, ?)
commit transaction
所有的结构化查询语言语句都会被预处理一次(对于每个连接 来说 ),然后被复用 。 这样的好处就是,避免结构 化查询语句注入问题,并且 可获得潜在的性能提升。
有两种查询数据库的方式。 可使用 Session::find<Dbo>( condition ) 来查询出单个 Dbo 类实例的数据库对象:
(tutorial1.C继续)
dbo :: ptr<User> joe = session . find < User >(). where ( "name = ?" ). bind ( "Joe" );
std :: cerr << "Joe has karma: " << joe -> karma << std :: endl ;
所有的查询都是预处理过的语句, 进行了 基于位置的参数绑定。 Session::find<T>()方法返回一个 Query< ptr<T> > 对象。Query对象可通过添加结构 化查询语言 where 、 order by 和 group by 定义的方式来对查询进行微调(refine), 还可以使用 Query::bind() 来绑定参数。 在这种情况下, 这个查询应当预期得到单个结果 ,并且 被直接转换成 一个数据库对象指针。
注意 |
从Wt 3.1.3 开始, Query 类具有了第二个参数 BindStrategy , 它有两个可能的取值,分别对应 于两种不同的查询实现方式 。 默认策略是动态绑定( DynamicBinding ), 这使得此查询成为一个长期存在的对象,与会话关联 到一起 ,并且 可以 多次运行。 同时,妳可以通过 修改排序规则、限制条件或偏移 量 来 修改查询条件 。 另一种策略是直接绑定( DirectBinding ), 它会将绑定的参数直接传递到 底层的一个预处理的语句中。 这也是Query 对象 在改版之前的标准行为。 这样的一个查询只能被运行一次,但是好处 就是(C++)开销较小 ,因为那些参数 的值都是直接传递给后端的,而不是储存 于查询对象之中的。 |
格式化之后发送给数据库的查询是这样的:
select id , version , "name" , "password" , "role" , "karma"
from "user"
where ( name = ?)
更通用的查询方式是使用 Session::query<Result>( sql ) , 它就不仅仅支持数据 库对象作为查询结果了。 以上的查询与下面的代码等价:
(tutorial1.C继续)
dbo :: ptr<User> joe2 = session . query < dbo :: ptr < User > >( "select u from user u" ). where ( "name = ?" ). bind ( "Joe" );
这将生成类似的结构化查询语言语句:
select u . id , u . version , u . "name" , u . "password" , u . "role" , u . "karma"
from user u
where ( name = ?)
传递给此方法的 sql 语句,可以是 其返回值 与 Result 类型兼容 的任何结构 化查询语言 语句。 这里的结构化查询语言语句中的 select 部分可以重写 ( 就样上面的示例中那样 ), 以返回 查询到的数据库对象的某些单个字段。
这里演示一下 , Session::query<Result>() 可用来返回其它类型的结果。研究 一下 以下的这个查询,它会返回一个 int 类型的结果。
(tutorial1.C继续)
int count = session . query < int >( "select count(1) from user" ). where ( "name = ?" ). bind ( "Joe" );
以上的这些查询都预期得到单一 的结果,但是查询 也可能返回多个结果的。所以, Session::query< Result >()可能会返回 一个 dbo::collection< Result > (对于多个结果 的情况 ), 而在以上的示例中 ,为了简单起见, 它们被强制转换成了单个 的 Result 。类似 地, Session::find< Dbo >()可能会返回一个 collection< ptr< Dbo > > 或一个单个的 ptr<_Dbo > 。如果 要求得到一个单个的结果, 而查询却找到 了多个结果,则 会抛出 NoUniqueResultException 异常。
collection< T > 是一个与标准模板 库(STL)兼容的集合 (collection), 它拥有 迭代器,实现 了 InputIterator 需求 。所以, 妳只能对一个集合中的结果遍历一次。 在那些结果被遍历过了之后, 这个 collection 对象便不可再使用 (但是对应 的 Query 对象可以被复用,除非使用 了直接绑定 ( DirectBinding ) 策略 )。
以下代码演示的是如何 对一个查询所返回的多个结果进行遍历:
(tutorial1.C继续)
typedef dbo :: collection< dbo::ptr<User> > Users ;
Users users = session . find < User >();
std :: cerr << "We have " << users . size () << " users:" << std :: endl ;
for ( Users :: const_iterator i = users . begin (); i != users . end (); ++ i )
std :: cerr << " user " << (* i )-> name
<< " with karma of " << (* i )-> karma << std :: endl ;
这段代码会进行两次数据库查询 : 一次是对应于 collection::size() 的调用, 另一次对应于结果 的遍历:
select count ( 1 ) from "user"
select id , version , "name" , "password" , "role" , "karma" from "user"
警告 |
一个查询对象会使用预先准备好的语句来执行,如果之前 没有为该查询准备过语句的话,会准备 一个新的语句。由于预先准备 的语句通常都不是可重入的, 同时呢,查询对象 在发现存在已有 的语句的时候会使用已有 的语句 ,所以, 妳需要注意, 不要 让两个对应于同一个语句的结果集合 同时处于活跃状态。所以, 在遍历某个查询的结果的时候, 妳不能 再 次使用该查询。所以 呢,也许 有必要在遍历之前将结果复制 到一个标准容器中(例如 std::vector )。 从Wt 3.1.3 开始, 会检测到并行使用 的情况,并且 会抛出异常,其内容为: A collection for '...' is already in use. Reentrant statement use is not yet implemented. 我们计划在以后的版本中移除 这个限制 , 在必要的情况下 对预先准备的语句进行复制。 |
与 大部分其它的智能指针不同, ptr< Dbo > 在默认情况下是只读的: 它返回一个 const Dbo * 。 要想 修改一个数据库对象的话,妳需要调用 ptr::modify() 方法, 该方法会返回一个非常量对象。 这会将该对象标记为脏的,并且 在稍后会将 对它的变更 同步到数据库中。
(tutorial1.C继续)
dbo :: ptr<User> joe = session . find < User >(). where ( "name = ?" ). bind ( "Joe" );
joe . modify ()-> karma ++;
joe . modify ()-> password = "public" ;
数据 库同步并不是立即发生的,相反 ,它们 会被延迟,直到: 显 式通过 ptr< Dbo >::flush() 或 Session::flush() 要求 同步; 某个查询被执行,而该查询 的结果可能会被已进行的修改影响;事务 被提交。
之前的代码会生成以下结构化查询语言语句:
select id , version , "name" , "password" , "role" , "karma"
from "user"
where ( name = ?)
update "user" set "version" = ?, "name" = ?, "password" = ?, "role" = ?, "karma" = ?
where "id" = ? and "version" = ?
我们已经了解过如何使用 Session::add(ptr< Dbo >) 了, 我们使用它向数据库里添加了一个新对象。 与之相对的操作是 ptr< Dbo >::remove() : 它会删除数据库中对应的对象。
(tutorial1.C继续)
dbo :: ptr<User> joe = session . find < User >(). where ( "name = ?" ). bind ( "Joe" );
joe . remove ();
在删除了一个对象之后,内存 中的副本对象仍然可以使用,甚至可以重新添加到数据库中去。
注意 |
就像 modify() 一样, add() 和 remove()操作也是延迟与数据库同步的,所以,下面 这段代码对于数据库不起任何作用: (tutorial1.C继续) dbo :: ptr<User> silly = session . add ( new User ()); silly . modify ()-> name = "Silly" ; silly . remove (); |
让我们来向勃客示例中添加文章 吧,并且定义文章 与用户之间多对一的关系。 在以下的代码中, 我们重点关注那些用来定义关系 的语句。
多对一关系(tutorial2.C)
#include <Wt/Dbo/Dbo>
#include <string>
namespace dbo = Wt :: Dbo ;
class User ;
class Post {
public :
...
dbo :: ptr<User> user ;
template < class Action >
void persist ( Action & a )
{
...
dbo :: belongsTo ( a , user , "user" );
}
} ;
class User {
public :
...
dbo :: collection< dbo::ptr<Post> > posts ;
template < class Action >
void persist ( Action & a )
{
...
dbo :: hasMany ( a , posts , dbo :: ManyToOne , "user" );
}
} ;
在 多 的一方, 我们加上对一个用户的引用,并且 在 persist() 方法 中调用 belongsTo() 。 这使得我们可以引用作为此篇文章的主人的那个用户。 最后一个参数,对应的就是定义了此关系的数据 库列的名字。
在 一 的一方, 我们加上 一个文章集合,并且 在 persist() 方法中调用 hasMany() 。此处 的连接(join)字段必须与前面所述(reciproce)的belongsTo()方法中的字段名相同。
如果我们也使用 Session::mapClass() 来将文章(Post)类添加到我们的会话中,并且创建数据库模式 的话,则会生成以下结构化查询语言语句:
create table "user" (
...
-- user 表不会受到此关系的影响
);
create table "post" (
...
"user_id" bigint ,
constraint "fk_post_user" foreign key ( "user_id" ) references "user" ( "id" )
)
注意, user_id 字段对应 于连接 名“user”。
在 多 的一方, 妳可以通过那个 ptr 对象来读写此文章所归属的用户。
在 一 的一方的那个集合,使得 我们可 以获取到所有关联的元素、插入 (insert())和删除(remove())元素。 其效果就跟在 多 的一方设置 ptr 是一样的。
示例:
(tutorial2.C继续)
dbo :: ptr<Post> post = session . add ( new Post ());
post . modify ()-> user = joe ; // 或者 joe.modify()->posts.insert(post);
// 会输出 'Joe has 1 post(s).'
std :: cerr << "Joe has " << joe -> posts . size () << " post(s)." << std :: endl ;
如妳所见, 一旦 将 joe 设置成 新的文章的用户 ( user ), 该文章也自动被反射(reflected)到 joe 的文章( posts )集合中去了, 反过来也一样。
警告 |
集合会使用一个预先准备的语句来执行。集合之间 会尝试着共享单个 的预先准备的语句 ,但是预先准备 的语句通常不是可重入的。由此导致 , 妳需要格外小心, 不要让对应于同一个语句的两个集合同时处于操作 (busy) 状态。因此 , 在遍历一个集合的时候, 妳需要注意不要再次遍历这同一个集合(无论是同一个对象或是另一个)。所以,也许 有必要在遍历之前 将结果复制到一个标准容器(例如 std::vector )中去。 我们计划在以后的版本中移除 这个限制 , 在必要的情况下 对预先准备的语句进行复制。 |
为了演示 多对多 关系, 我们向勃客示例中加入标签(tags),并且 在文章和标签之间定义一个 多对多 关系。 在以下的代码中, 我们依然重点关注那些用来定义关系 的语句。
多对多关系(tutorial2.C)
#include <Wt/Dbo/Dbo>
#include <string>
namespace dbo = Wt :: Dbo ;
class Tag ;
class Post {
public :
...
dbo :: collection< dbo::ptr<Tag> > tags ;
template < class Action >
void persist ( Action & a )
{
...
dbo :: hasMany ( a , tags , dbo :: ManyToMany , "post_tags" );
}
} ;
class Tag {
public :
...
dbo :: collection< dbo::ptr<Post> > posts ;
template < class Action >
void persist ( Action & a )
{
...
dbo :: hasMany ( a , posts , dbo :: ManyToMany , "post_tags" );
}
} ;
聪明的妳想必早已猜到了, 在两个类中,关系是几乎同样地反映出来的: 它们都拥有 一个集合 ( collection ),包含 了相关的类的数据库对象, 而在 persist() 方法中,我们调用 hasMany() 。 在这种情况下,所使用的连接字段 对应于用来表示 此关系 的连接表的名字。
使用Session::mapClass() 来将文章 (Post)(☯: 这里可能是Tag,标签类 )类添加到我们的会话中 。现在 , 在创建数据库模式的时候,会生成以下的结构化查询语言语句:
create table "post" (
...
-- post 表不受此关系的影响
)
create table "tag" (
...
-- tag 表不受此关系的影响
)
create table "post_tags" (
"post_id" bigint not null ,
"tag_id" bigint not null ,
primary key ( "post_id" , "tag_id" ),
constraint "fk_post_tags_key1" foreign key ( "post_id" ) references "post" ( "id" ),
constraint "fk_post_tags_key2" foreign key ( "tag_id" ) references "tag" ( "id" )
)
create index "post_tags_post" on "post_tags" ( "post_id" )
create index "post_tags_tag" on "post_tags" ( "tag_id" )
在这个 多对多 关系中, 每一方的集合都允许我们获取所有 的关联元素。然而 ,与 一个 多对一 关系 中的集合不同的是, 我们现在可以对集合插入 ( insert() )和删除( erase() )其中 的 条目。 要定义一篇文章和一个标签之间的关系的话,妳需要 做以下两件事之一: 将文章添加到 该标签的 posts 集合中;或者 将该标签添加到该文章的 tags 集合中。 妳不能两件事都做 !变更 会自动反射到对方对象 的集合中。 同样地, 要想断开一篇文章与一个标签之间的关系的话, 妳应当 从文章的 tags 集合中删除该标签,或者从该标签的 posts 集合中删除该文章 ,但不要在两个集合中都删除对方。
示例:
(tutorial2.C继续)
dbo :: ptr<Post> post = ...
dbo :: ptr<Tag> cooking = session . add ( new Tag ());
cooking . modify ()-> name = "Cooking" ;
post . modify ()-> tags . insert ( cooking );
// 会输出 '1 post(s) tagged with Cooking.'
std :: cerr << cooking -> posts . size () << " post(s) tagged with Cooking." << std :: endl ;
警告 |
参考之前的警告。 |
一对一 关系目前是不被支持的,但是 可以通过 多对一 关系来模拟,因为它们 的数据库模式实际上是相同的。
默认情况下, Wt::Dbo 会向每个被映射的表中添加这两样东西: 一个自动增加的代理主 键( id )和一个版本字段 ( version )。
当然,对于 新项目来说 ,这些默认行为狠有意义。但是 呢, 妳仍然可以 对映射情况进行微调,以适应任何已有 的数据库模式。
要想改变 一个被映射的类 的代理主键的字段名,或者 想禁用某个 类的 代理 主键而使用 一个自然主键的话, 妳需要特化 Wt::Dbo::dbo_traits<C> 模板。
例如,以下的代码,将Post 类的 主键字段 从 id 改成了 post_id :
修改"id"字段名(tutorial3.C)
#include <Wt/Dbo/Dbo>
namespace dbo = Wt :: Dbo ;
class Post {
public :
...
} ;
namespace Wt {
namespace Dbo {
template <>
struct dbo_traits < Post > : public dbo_default_traits {
static const char * surrogateIdField () {
return "post_id" ;
}
} ;
}
}
要想改变乐观 锁 并行控制版本字段 ( version )的字段名,或者干脆禁用某个 类的乐观 锁并行控制功能的话, 妳需要特化 Wt::Dbo::dbo_traits<C> 模板。
例如,以下的代码禁用了Post 类的乐观 锁并行控制功能:
禁用"version"字段名(tutorial4.C)
#include <Wt/Dbo/Dbo>
namespace dbo = Wt :: Dbo ;
class Post {
public :
...
} ;
namespace Wt {
namespace Dbo {
template <>
struct dbo_traits < Post > : public dbo_default_traits {
static const char * versionField () {
return 0 ;
}
} ;
}
}
妳可能不想使用自动增加的代理主键,而是使用 一个不同的主键。
例如,以下的代码, 将User 表 的主键改成一个字符串 ( 它的用户名 (username) (☯: 从下面的代码来看, 应当 是userId,用户编号 ) ),并且 被映射为 一个 叫做 user_name ( ☯: 从下面的代码来看, 应当 是user_ id ) 的 varchar (20) 字段:
使用一个自然主键(tutorial5.C)
#include <Wt/Dbo/Dbo>
namespace dbo = Wt :: Dbo ;
class User {
public :
std :: string userId ;
template < class Action >
void persist ( Action & a )
{
dbo :: id ( a , userId , "user_id" , 20 );
}
} ;
namespace Wt {
namespace Dbo {
template <>
struct dbo_traits < User > : public dbo_default_traits {
typedef std :: string IdType ;
static IdType invalidId () {
return std :: string ();
}
static const char * surrogateIdField () { return 0 ; }
} ;
}
}
id()函数的语法与 field() 函数一致。
这里用的自然主键也可以是 :一个复合(composite)键、一个外键或一个组合(combination)。
要想将一个复合类型用作自然主键的话, 也就是说,用那种 由多个字段组成的类型作为主键, 妳需要写出 一个 对应 的C++类型。
对这个类型有一庹基本要求:默认构造函数 啊、比较操作 符(== 和 <)啊、流(streaming)操作符啊。
使用一个复合型自然主键(tutorial6.C)
struct Coordinate {
int x , y ;
Coordinate ()
: x (- 1 ), y (- 1 ) { }
Coordinate ( int an_x , int an_y )
: x ( an_x ), y ( an_y ) { }
bool operator == ( const Coordinate & other ) const {
return x == other . x && y == other . y ;
}
bool operator < ( const Coordinate & other ) const {
if ( x < other . x )
return true ;
else if ( x == other . x )
return y < other . y ;
else
return false ;
}
} ;
std :: ostream & operator << ( std :: ostream & o , const Coordinate & c )
{
return o << "(" << c . x << ", " << c . y << ")" ;
}
然后,妳必须说明如何对这个类型进行持久化: 为它重载Dbo 的 field() 函数。
(tutorial6.C继续)
namespace Wt {
namespace Dbo {
template < class Action >
void field ( Action & action , Coordinate & coordinate , const std :: string & name ,
int size = - 1 )
{
field ( action , coordinate . x , name + "_x" );
field ( action , coordinate . y , name + "_y" );
}
}
}
以上东西做了之后,我们就可以使用 Coordinate 类型来作为自然主键类型了:
(tutorial6.C继续)
class GeoTag ;
namespace Wt {
namespace Dbo {
template <>
struct dbo_traits < GeoTag > : public dbo_default_traits
{
typedef Coordinate IdType ;
static IdType invalidId () { return Coordinate (); }
static const char * surrogateIdField () { return 0 ; }
} ;
}
}
class GeoTag {
public :
Coordinate position ;
std :: string name ;
template < class Action >
void persist ( Action & a )
{
dbo :: id ( a , position , "position" );
dbo :: field ( a , name , "name" );
}
} ;
注意,复合 型主键 中 还可以包含外键的, 只需要 在复合类型中储存ptr<>对象 ,并且使用 belongsTo() 声明 来将它们映射好就可以。参考 tutorial8.C ,那是一个完整的示例。
belongsTo()函数是重载的,所以 妳可以将那些 由数据库强制 要求的外键约束添加进去,例如:
•. NotNull: 不能为空(null)
•. OnUpdateCascade: 把对于 (自然)主键的变更级联地传递(cascade)到引用 它的外键上
•. OnUpdateSetNull: 当更新一个(自然)主键时,将引用它的那些外键设置成空
•. OnDeleteCascade: 删除一个对象时,级联 地删除(cascade)那些 以外键引用到此对象的对象
•. OnDeleteSetNull: 删除一个对象时,将那些引用 此对象的外键设置成空 。
下一章中, 我们将看到,如何 为那些同时(double as)也是主键的外键指定外键约束 。
让我们来定义一个UserInfo 类,它提供关于用户 (User)的附加信息。 对于每个用户 (User) ,我们只允许有一个用户信息(UserInfo)对象,因此 , 把对用户 (User)的引用就作为用户信息 (UserInfo)的主 键 。
使用外键作为主键(tutorial7.C)
#include <Wt/Dbo/Dbo>
#include <Wt/Dbo/backend/Sqlite3>
namespace dbo = Wt :: Dbo ;
class UserInfo ;
class User ;
namespace Wt {
namespace Dbo {
template <>
struct dbo_traits < UserInfo > : public dbo_default_traits {
typedef ptr<User> IdType ;
static IdType invalidId () {
return ptr < User >();
}
static const char * surrogateIdField () { return 0 ; }
} ;
}
}
class User {
public :
std :: string name ;
dbo :: collection< dbo::ptr<UserInfo> > infos ;
template < class Action >
void persist ( Action & a )
{
dbo :: field ( a , name , "name" );
// 事实 上, 这个关系 会被约束为 hasOne() ...
dbo :: hasMany ( a , infos , dbo :: ManyToOne , "user" );
}
} ;
class UserInfo {
public :
dbo :: ptr<User> user ;
std :: string info ;
template < class Action >
void persist ( Action & a )
{
dbo :: id ( a , user , "user" , dbo :: OnDeleteCascade );
dbo :: field ( a , info , "info" );
}
} ;
void run ()
{
/*
* 设置 一个会话, 一般情况下, 只需要在程序启动时做一次 。
*/
dbo :: backend :: Sqlite3 sqlite3 ( ":memory:" );
sqlite3 . setProperty ( "show-queries" , "true" );
dbo :: Session session ;
session . setConnection ( sqlite3 );
session . mapClass < User >( "user" );
session . mapClass < UserInfo >( "user_info" );
/*
* 尝试创建数据库模式 (如果已 经存在,则会失败 )。
*/
session . createTables ();
dbo :: Transaction transaction ( session );
{
User * user = new User ();
user -> name = "Joe" ;
dbo :: ptr<User> userPtr = session . add ( user );
UserInfo * userInfo = new UserInfo ();
userInfo -> user = userPtr ;
userInfo -> info = "great guy" ;
session . add ( userInfo );
transaction . commit ();
}
{
dbo :: Transaction transaction ( session );
dbo :: ptr<UserInfo> info = session . find < UserInfo >();
std :: cerr << info -> user -> name << " is a " << info -> info << std :: endl ;
transaction . commit ();
}
}
int main ( int argc , char ** argv )
{
run ();
}
如妳所见, 这个示例中, 我们实际上需要的是一对一的关系,但是Dbo 中目前不支持一对一的关系,于是 我们就使用多对一的关系来模拟了(实际 上这两种关系在结构化查询语言中的表示方式是相同的 )。
运行的时候,会输出这些东西:
begin transaction
create table "user" (
"id" integer primary key autoincrement ,
"version" integer not null ,
"name" text not null
)
create table "user_info" (
"version" integer not null ,
"user_id" bigint ,
"info" text not null ,
primary key ( "user_id" ),
constraint "fk_user_info_user" foreign key ( "user_id" ) references "user" ( "id" ) on delete cascade
)
commit transaction
begin transaction
insert into "user" ( "version" , "name" ) values (?, ?)
insert into "user_info" ( "version" , "user_id" , "info" ) values (?, ?, ?)
commit transaction
begin transaction
select version , "user_id" , "info" from "user_info"
select "version" , "name" from "user" where "id" = ?
Joe is a great guy
commit transaction
从数据库中读取数据,或者向数据库中刷新变更,都需要具有 一个活跃的事务。事务 ( Transaction )是一个资源初始化即占用(RIIA (Resource-Initialization-is-Acquisition))类, 它会同时保证以下两点:并发会话之间 的隔离; 向数据库提交变更的原子性。
这个库实现了乐观锁, 这就使得可以检测( 而不是避免 )并发 的修改。 这是一种广受建议并且广泛使用的策略,用来 在一种 可扩展的尺度 上处理并发问题, 而不 用向数据库中加入 写锁(write locks)。 为了检测到并 发 修改, 我们向每个表中加入了一个 version 字段, 这个字段 的值 在每次修改时都会增 大 。 在进行修改(例如更新 或删除某个对象 )时, 会检查数据库 中该记录的版本 ,确认 一下它是不是 与最初从数据库里读取到的对象的版本一致。
注意 |
事务隔离级别 这个库的 乐观 锁 策略 所要求的最小隔离级别是 提交 读(Read Committed) : 在一个事务中进行的变更,只有在它提交过之后才立即变得 对其它会话可 见 。 这通常是一个数据库所支持的最低隔离级别。 |
Transaction 是一个轻量级的代理, 它引用了一个 逻辑 事务:多个 (通常 是嵌套的 )事务(Transaction)对象 可 同时 (simultaneously)被 实例化 , 在这些事务都被提交之后,逻辑事务才会 被提交。通过 这种方式, 妳可以轻易地保护那些需要 带 这种事务对象 的数据库访问能力的单 个函数, 事务会在 找得到更 大规模 (wider)的事务的情况下自动加入其中。事实 上, 事务对象 会延迟 到必要 的时候才 打开一个真正的事务,所以 , 妳大可以 为了确保某 段代码 是原子的而实例 化一个事务对象, 这不会有什么性能损失 ,即使 妳并不确定 是否会真正操作数据库也没关系。
事务是有可能失败的, 对失败的事务进行处理是一个群策群力( ☯: 本座认为 意思 是指 库和程序猿都要出力,原文是integral aspect )的过程。 当库检测到一次并发修改时, 会抛出 StaleObjectException 异常。 还可能会抛出其它的一些异常,包括后端驱动 的一些异常,例如 :数据库模式 与当前映射 不兼容。 还有可能,业务逻辑检测 到了某些问题,然后抛出异常,导致事务 被回滚。 当一个事务被回滚之后, 被修改过的数据库对象 就没有 被成功地 同步到数据库中,但是 可以在稍后 的新事务中 同步。
当然了,狠多异常都是致命的。然而 ,有个异常是值得格外关注的: StaleObjectException 。 可采用不同的策略来处置这个异常。无论采用什么处理方法 , 妳最少应当重新读取 ( reread() )一下 已经过期 (stale)的数据库对象,然后才 能 在新的事务中提交变更 。
Wt::Dbo 是包含在Wt 中的,所以 可作为这个库的一部分安装 。并且 妳的操作系统可能已经 有这样一个标准软件包了。
然而,这个库根本 不依赖Wt,因而 可以单独编译 、安装及使用。利用 一个Wt 源代码包(并且 在一个类UNIX 的环境中 ), 妳可以按以下步骤来仅仅编译安装 Wt::Dbo :
从源代码安装 Wt::Dbo ( 类 UNIX系统)
$ cd wt-xxx
$ mkdir build
$ cd build
$ cmake ../ # 可能需要其它 的选项,用来寻找boost、postgres,设置安装目录,……
$ cd src/Wt/Dbo
$ make
$ sudo make install
参考Wt安装向导 。
Olivia Alaina May
Olivia Alaina May
动了外科手术的鳄鱼
烧过的公交车
海怪
唐莉
贝克汉
HxLauncher: Launch Android applications by voice commands